package com.fengye.security.rest;

import com.fengye.security.config.AppProperties;
import com.fengye.security.domain.MfaType;
import com.fengye.security.domain.User;
import com.fengye.security.domain.dto.*;
import com.fengye.security.domain.vo.TokenVo;
import com.fengye.security.exception.*;
import com.fengye.security.security.filter.JwtAuthenticationToken;
import com.fengye.security.security.service.UserCacheService;
import com.fengye.security.security.service.UserDetailsServiceImpl;
import com.fengye.security.security.service.UserValidationService;
import com.fengye.security.security.service.mfa.EmailService;
import com.fengye.security.security.service.mfa.LogService;
import com.fengye.security.security.service.mfa.SmsService;
import com.fengye.security.utils.JwtUtil;
import com.fengye.security.utils.TotpUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

/**
 * @author fengyexjtu@126.com
 * @date 2022年04月13日 11:04 AM
 */

@RestController
@RequestMapping("/authorize")
@RequiredArgsConstructor
//@AllArgsConstructor
//@NoArgsConstructor
@Slf4j
public class AuthorizeResource {

    private final AppProperties appProperties;

    private final JwtUtil jwtUtil;


    private final UserDetailsServiceImpl userService;
    private final MessageSource messageSource;

    private final UserValidationService userValidationService;

    private final UserCacheService userCacheService;

    private final SmsService smsService;
    private final LogService logService;
    private final EmailService emailService;
    private final TotpUtil totpUtil;

    @PostMapping("/register")
    public void register(@Valid @RequestBody UserDto userDto, Locale locale) {
        if (userService.isUsernameExisted(userDto.getUsername())) {
            throw new DuplicateProblem(messageSource.getMessage(
                    "Exception.duplicate.title", null, locale),
                    messageSource.getMessage("Exception.duplicate.username", null, locale));
        }
        if (userService.isEmailExisted(userDto.getEmail())) {
            throw new DuplicateProblem(messageSource.getMessage(
                    "Exception.duplicate.title", null, locale),
                    messageSource.getMessage("Exception.duplicate.email", null, locale));
        }
        if (userService.isMobileExisted(userDto.getMobile())) {
            throw new DuplicateProblem(messageSource.getMessage(
                    "Exception.duplicate.title", null, locale),
                    messageSource.getMessage("Exception.duplicate.mobile", null, locale));
        }
        val user = User.builder()
                .username(userDto.getUsername())
                .name(userDto.getName())
                .email(userDto.getEmail())
                .mobile(userDto.getMobile())
                .password(userDto.getPassword())
                .enabled(false)
                .accountNonExpired(false)
                .credentialsNonExpired(false)
                .accountNonLocked(false)
                .usingMfa(true)
                .mfaKey(totpUtil.encodeKeyToString())
                .build();
        userService.register(user);
    }

    @PutMapping("/totp")
    public ResponseEntity<?> sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto) {
        Optional<Pair<User, String>> userStringPair = userCacheService.retrieveUser(sendTotpDto.getMfaId())
                .flatMap(user -> userService.createTotp(user).map(code -> Pair.of(user, code)));
        Pair<User, String> pair = userStringPair.orElseThrow(InvalidTotpProblem::new);
        log.debug("totp: {}", pair.getSecond());
        if (sendTotpDto.getMfaType() == MfaType.SMS) {
            smsService.send(pair.getFirst().getMobile(), pair.getSecond());
        } else if (sendTotpDto.getMfaType() == MfaType.EMAIL){
            emailService.send(pair.getFirst().getEmail(), pair.getSecond());
        }else {
            logService.send(pair.getFirst().getId().toString(), pair.getSecond());
        }

        // userStringPair
        //         .ifPresentOrElse(pair -> {
        //             log.debug("totp: {}", pair.getSecond());
        //             if (sendTotpDto.getMfaType() == MfaType.SMS) {
        //                 smsService.send(pair.getFirst().getMobile(), pair.getSecond());
        //             } else if (sendTotpDto.getMfaType() == MfaType.EMAIL){
        //                 emailService.send(pair.getFirst().getEmail(), pair.getSecond());
        //             }else {
        //                 logService.send(pair.getFirst().getId().toString(), pair.getSecond());
        //             }
        //             result =  Map.of("mfaId", sendTotpDto.getMfaId(), "code", pair.getSecond());
        //         }, () -> {
        //             throw new InvalidTotpProblem();
        //         });
        return ResponseEntity.ok().body(Map.of("mfaId", sendTotpDto.getMfaId(), "code", pair.getSecond()));
    }

    @PostMapping("/totp")
    public TokenVo verifyTotp(@Valid @RequestBody TotpVerificationDto totpVerificationDto) {
        return userCacheService.verifyTotp(totpVerificationDto.getMfaId(), totpVerificationDto.getCode())
                .map(User::getUsername)
                .flatMap(userService::findOptionalByUsername)
                .map(userService::loginWithTotp)
                .orElseThrow(InvalidTotpProblem::new);
    }

    @PostMapping("/token")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDto loginDTO) {
        return userService.findOptionalByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
                .map(user -> {
                    userService.upgradePasswordEncodingIfNeeded(user, loginDTO.getPassword());
                    if (!user.isEnabled()) {
                        throw new UserNotEnabledProblem();
                    }
                    if (!user.isAccountNonLocked()) {
                        throw new UserAccountLockedProblem();
                    }
                    if (!user.isAccountNonExpired()) {
                        throw new UserAccountExpiredProblem();
                    }
                    // 不使用多因子认证
                    if (!user.isUsingMfa()) {
                        return ResponseEntity.ok().body(userService.login(user));
                    }
                    // 使用多因子认证
                    val mfaId = userCacheService.cacheUser(user);
                    return ResponseEntity
                            .status(HttpStatus.UNAUTHORIZED)
                            .header("X-Authenticate", "mfa", "realm=" + mfaId)
                            .build();
                })
                .orElseThrow(BadCredentialProblem::new);
    }

    private TokenVo createTokenVo (UserDetails user) {
        TokenVo tokenVo = new TokenVo();
        tokenVo.setAccessToken(jwtUtil.createAccessToken(user));
        tokenVo.setRefreshToken(jwtUtil.createRefreshToken(user));
        return tokenVo;
    }

    @PostMapping("/refresh/token")
    public TokenVo refreshToken(@RequestParam String refreshToken, @RequestHeader("Authorization") String token){
        String accessToken = token.replace(appProperties.getJwt().getPrefix(), "");
        log.info("accessToken = {}", accessToken);
        log.info("refreshToken = {}", refreshToken);
        if (jwtUtil.validAccessToken(accessToken)){
            throw new AccessDeniedException("accessToken错误");
        }
        TokenVo tokenVo = new TokenVo();
        if (jwtUtil.validRefreshToken(refreshToken)){
            tokenVo.setAccessToken(jwtUtil.createAccessTokenByRefreshToken(refreshToken));
            tokenVo.setRefreshToken(refreshToken);
            return tokenVo;
        }
        throw new BadCredentialsException("refreshToken 错误");


    }

    @GetMapping("/validation/username")
    public boolean validateUsername(@RequestParam String username) {
        return userValidationService.isUsernameExisted(username);
    }

    @GetMapping("/validation/email")
    public boolean validateEmail(@RequestParam String email) {
        return userValidationService.isEmailExisted(email);
    }

    @GetMapping("/validation/mobile")
    public boolean validateMobile(@RequestParam String mobile) {
        return userValidationService.isMobileExisted(mobile);
    }

}
